/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.hibernate.criteria;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;

import com.genia.toolbox.basics.bean.Delimiter;
import com.genia.toolbox.basics.bean.DisplayObject;
import com.genia.toolbox.basics.bean.Pair;
import com.genia.toolbox.basics.bean.impl.ConstantDelimiter;
import com.genia.toolbox.basics.exception.technical.TechnicalIOException;
import com.genia.toolbox.basics.manager.CollectionManager;
import com.genia.toolbox.persistence.basis.criteria.AbstractCriteria;
import com.genia.toolbox.persistence.bean.Persistable;
import com.genia.toolbox.persistence.criteria.CriteriaType;
import com.genia.toolbox.persistence.criteria.PropertyField;
import com.genia.toolbox.persistence.criteria.field.Field;
import com.genia.toolbox.persistence.hibernate.bean.Queryable;
import com.genia.toolbox.persistence.hibernate.bean.impl.QueryableDisplayObject;
import com.genia.toolbox.persistence.hibernate.criteria.criterion.DefaultHibernateCriterion;
import com.genia.toolbox.persistence.hibernate.criteria.criterion.HibernateCriterion;
import com.genia.toolbox.persistence.hibernate.criteria.field.HibernateField;
import com.genia.toolbox.persistence.hibernate.criteria.order.HibernateOrder;
import com.genia.toolbox.persistence.hibernate.manager.HibernatePersistenceManager;
import com.genia.toolbox.persistence.manager.PersistenceManager;

/**
 * the hibernate implementation of <code>Criteria</code>.
 * 
 * @param <MAINTYPE>
 *          the main type of objects to retrieve
 */
public class HibernateCriteria<MAINTYPE>
    extends AbstractCriteria<MAINTYPE>
    implements Queryable
{

  /**
   * this variable is an always increasing long that generates new aliases for
   * the queries.
   */
  private static long nbAlias = 0;

  /**
   * the <code>Delimiter</code> for the Order By part of the query.
   */
  private static final Delimiter ORDER_BY_DELIMITER = new ConstantDelimiter(" ORDER BY ", ", ", "");

  /**
   * the <code>Delimiter</code> for the Group By part of the query.
   */
  private static final Delimiter GROUP_BY_DELIMITER = new ConstantDelimiter(" GROUP BY ", ", ", "");

  /**
   * the <code>Delimiter</code> for the From part of the query.
   */
  private static final Delimiter FROM_DELIMITER = new ConstantDelimiter(" FROM ", ", ", "");

  /**
   * the <code>Delimiter</code> for the From part of the query.
   */
  private static final Delimiter FROM_INNER_JOIN_DELIMITER = new ConstantDelimiter(" INNER JOIN ", " INNER JOIN ", "");

  /**
   * the <code>Delimiter</code> for the Select part of the query.
   */
  private static final Delimiter SELECT_DELIMITER = new ConstantDelimiter("SELECT ", ", ", "");

  /**
   * the <code>Delimiter</code> for the Select part of the query.
   */
  private static final Delimiter SELECT_DISTINCT_DELIMITER = new ConstantDelimiter("SELECT DISTINCT ", ", ", "");

  /**
   * a {@link DisplayObject} used to display the usual select when needed.
   */
  private static final DisplayObject<HibernateCriteriaType<?>> SIMPLE_SELECT_PRINTER = new DisplayObject<HibernateCriteriaType<?>>()
  {

    public void append(Appendable appendable, HibernateCriteriaType<?> object)
        throws IOException
    {
      appendable.append(object.getAlias());
    }

  };

  /**
   * a {@link DisplayObject} used to display the usual select when needed.
   */
  private static final DisplayObject<Entry<HibernateCriteriaType<?>, Pair<HibernateCriteriaType<?>, PropertyField<?, ?>>>> INNER_JOIN_PRINTER = new DisplayObject<Entry<HibernateCriteriaType<?>, Pair<HibernateCriteriaType<?>, PropertyField<?, ?>>>>()
  {

    public void append(Appendable appendable, Entry<HibernateCriteriaType<?>, Pair<HibernateCriteriaType<?>, PropertyField<?, ?>>> object)
        throws IOException
    {
      appendable.append(object.getValue().getFirstElement().getAlias()).append(".").append(object.getValue().getSecondElement().getPropertyName()).append(" AS ").append(object.getKey().getAlias());
    }

  };



  /**
   * return a new value to generate aliases.
   * 
   * @return a new value to generate aliases
   */
  private static long getNextAlias()
  {
    synchronized (HibernateCriteria.class) {
      return nbAlias++;
    }
  }

  /**
   * the <code>StringBuilder</code> used to generate the query.
   */
  private transient StringBuilder stringBuilder = null;

  /**
   * the parameters of this query. It allows to handle constant values.
   */
  private transient Map<String, Object> parameters = null;

  /**
   * the <code>DisplayObject</code> used to generate textual representation of
   * <code>Queryable</code> that can use named parameters.
   */
  private transient QueryableDisplayObject parametrizableQueryableDisplayObject = null;

  /**
   * the <code>DisplayObject</code> used to generate textual representation of
   * <code>Queryable</code> that cannot use named parameters.
   */
  private transient QueryableDisplayObject unParametrizableQueryableDisplayObject = null;

  /**
   * the base criteria (before projection) of this query.
   */
  private transient HibernateCriteria<?> baseCriteria = null;



  /**
   * Constructor that allow to create projections.
   * 
   * @param modele
   *          the original <code>Criteria</code>
   * @param mainCriteriaType
   *          the <code>CriteriaType</code> of the main element of the
   *          projection.
   * @param field
   *          the <code>Field</code> representing the main projection
   */
  public HibernateCriteria(final HibernateCriteria<?> modele, final CriteriaType<MAINTYPE> mainCriteriaType, final Field field)
  {
    super(modele, mainCriteriaType, field);
    this.baseCriteria = modele;
  }



  /**
   * Constructor.
   * 
   * @param persistenceManager
   *          the <code>HibernatePersistenceManager</code> that is used by
   *          this implementation
   * @param mainType
   *          the class of the main type of this <code>Criteria</code>
   */
  public HibernateCriteria(final PersistenceManager persistenceManager, final Class<MAINTYPE> mainType)
  {
    super(persistenceManager, mainType);
  }



  /**
   * append to stringBuilder the part of the query represented by this object.
   * 
   * @param canHaveParameter
   *          can this part of the query use named parameters?
   * @param collectionManager
   *          The <code>CollectionManager</code> that allow us to perform
   *          joins
   * @param criteria
   *          the <code>HibernateCriteria</code> the represents the all query
   * @param stringBuilder
   *          the <code>StringBuilder</code> to append to.
   * @param queryableDisplayObject
   *          the <code>DisplayObject</code> that allows to convert this
   *          <code>Queryable</code> to a <code>String</code>
   */
  public void appendQuery(final boolean canHaveParameter, final CollectionManager collectionManager, final HibernateCriteria<?> criteria, final StringBuilder stringBuilder,
      final QueryableDisplayObject queryableDisplayObject)
  {
    stringBuilder.append(getQuery());
  }



  /**
   * Fill the parameters <code>Map</code> with the constants elements of this
   * query.
   * 
   * @param parameters
   *          the <code>Map</code> to be filled
   */
  public void fillParameters(final Map<String, Object> parameters)
  {
    parameters.putAll(getParameters());
  }



  /**
   * return the main type of the base criteria (before projection) of this
   * query.
   * 
   * @return the main type of the base criteria (before projection) of this
   *         query
   */
  public HibernateCriteriaType<?> getBaseMainType()
  {
    if (baseCriteria == null) {
      return (HibernateCriteriaType<MAINTYPE>) getMainType();
    }
    return baseCriteria.getBaseMainType();
  }



  /**
   * getter for the collectionManager property.
   * 
   * @return the collectionManager
   */
  public CollectionManager getCollectionManager()
  {
    return ((HibernatePersistenceManager) getPersistenceManager()).getCollectionManager();
  }



  /**
   * Return the parameters of this query indexed by their alias.
   * 
   * @return the parameters of this query indexed by their alias
   */
  public Map<String, Object> getParameters()
  {
    if (parameters == null) {
      parameters = new TreeMap<String, Object>();
      fillParameters(parameters, getGroupBys());
      fillParameters(parameters, getOrders());
      if (getRestrictions() != null) {
        getRestrictions().fillParameters(parameters);
      }
    }
    return parameters;
  }



  /**
   * return the HQL query to use with hibernate.
   * 
   * @return the HQL query to use with hibernate
   */
  public String getQuery()
  {
    synchronized (HibernateCriteria.class) {
      if (stringBuilder == null) {
        stringBuilder = new StringBuilder();
        try {
          appendSelect();
          appendFrom();
          appendWhere();
          appendGroupBy();
          appendOrderBy();
        }
        catch (final TechnicalIOException ioException) {
          assert false : "Can't happen";
        }
      }
      return stringBuilder.toString();
    }
  }



  /**
   * return <code>true</code> if and only if this query returns more than one
   * object per line.
   * 
   * @return <code>true</code> if and only if this query returns more than one
   *         object per line
   */
  public boolean isJoined()
  {
    if (getProjections() != null && !getProjections().isEmpty()) {
      return getProjections().size() > 1;
    }
    return getBaseQuery().size() > 1;
  }



  /**
   * Create a new <code>Criteria</code> that is the projection of this
   * <code>Criteria</code> on the <code>Field</code> passed in argument.
   * 
   * @param <TYPE>
   *          the type of the field to project to
   * @param type
   *          the class of the type of the field to project to
   * @param field
   *          the field to project to
   * @return the new <code>Criteria</code> representing the projected query.
   */
  public <TYPE> HibernateCriteria<TYPE> project(final Class<TYPE> type, final Field field)
  {
    return new HibernateCriteria<TYPE>(this, newCriteriaType(type), field);
  }



  /**
   * Create a new <code>Criteria</code> that is the projection of this
   * <code>Criteria</code> on the object defined by a
   * <code>CriteriaType</code>. This method is used only to project to a type
   * that has been created with <code>join</code>.
   * 
   * @param <TYPE>
   *          the type of the field to project to
   * @param criteriaType
   *          the <code>CriteriaType</code> of the field to add in the query.
   *          This <code>CriteriaType</code> must already be an existing
   *          <code>CriteriaType</code> of this criteria.
   * @return the new <code>Criteria</code> representing the projected query.
   */
  public <TYPE> HibernateCriteria<TYPE> project(final CriteriaType<TYPE> criteriaType)
  {
    return new HibernateCriteria<TYPE>(this, criteriaType, getPersistenceManager().getFieldManager().getObjectField(criteriaType));
  }



  /**
   * getter for the baseQuery property.
   * 
   * @return the baseQuery
   * @see com.genia.toolbox.persistence.basis.criteria.AbstractCriteria#getBaseQuery()
   */
  @SuppressWarnings("unchecked")
  @Override
  protected List<HibernateCriteriaType<? extends Persistable<?>>> getBaseQuery()
  {
    return (List<HibernateCriteriaType<? extends Persistable<?>>>) super.getBaseQuery();
  }



  /**
   * getter for the groupBys property.
   * 
   * @return the groupBys
   * @see com.genia.toolbox.persistence.basis.criteria.AbstractCriteria#getGroupBys()
   */
  @SuppressWarnings("unchecked")
  @Override
  protected List<HibernateField> getGroupBys()
  {
    return (List<HibernateField>) super.getGroupBys();
  }



  /**
   * getter for the orders property.
   * 
   * @return the orders
   * @see com.genia.toolbox.persistence.basis.criteria.AbstractCriteria#getOrders()
   */
  @SuppressWarnings("unchecked")
  @Override
  protected List<HibernateOrder> getOrders()
  {
    return (List<HibernateOrder>) super.getOrders();
  }



  /**
   * getter for the projections property.
   * 
   * @return the projections
   * @see com.genia.toolbox.persistence.basis.criteria.AbstractCriteria#getProjections()
   */
  @SuppressWarnings("unchecked")
  @Override
  protected Map<HibernateCriteriaType<?>, HibernateField> getProjections()
  {
    return (Map<HibernateCriteriaType<?>, HibernateField>) super.getProjections();
  }



  /**
   * getter for the innerJoins property.
   * 
   * @return the innerJoins
   */
  @SuppressWarnings("unchecked")
  @Override
  protected Map<HibernateCriteriaType<?>, Pair<HibernateCriteriaType<?>, PropertyField<?, ?>>> getInnerJoins()
  {
    return (Map<HibernateCriteriaType<?>, Pair<HibernateCriteriaType<?>, PropertyField<?, ?>>>) super.getInnerJoins();
  }



  /**
   * getter for the restrictions property.
   * 
   * @return the restrictions
   * @see com.genia.toolbox.persistence.basis.criteria.AbstractCriteria#getRestrictions()
   */
  @Override
  protected DefaultHibernateCriterion getRestrictions()
  {
    return (DefaultHibernateCriterion) super.getRestrictions();
  }



  /**
   * returns a new <code>HibernateCriteriaType</code> associated to the type
   * given in paramters.
   * 
   * @param <TYPE>
   *          the type of the <code>HibernateCriteriaType</code>
   * @param type
   *          the class of the type of the <code>HibernateCriteriaType</code>
   * @return a new <code>HibernateCriteriaType</code> associated to the type
   *         given in paramters
   */
  @Override
  protected <TYPE> HibernateCriteriaType<TYPE> newCriteriaType(final Class<TYPE> type)
  {
    return new HibernateCriteriaType<TYPE>(type, "a" + getNextAlias());
  }



  /**
   * append the From part of the query to <code>stringBuilder</code>.
   * 
   * @throws TechnicalIOException
   *           never
   */
  private void appendFrom()
      throws TechnicalIOException
  {
    getCollectionManager().join(stringBuilder, getBaseQuery(), FROM_DELIMITER, getParametrizableQueryableDisplayObject());
    if (!getInnerJoins().isEmpty()) {
      getCollectionManager().join(stringBuilder, getInnerJoins().entrySet(), FROM_INNER_JOIN_DELIMITER, INNER_JOIN_PRINTER);
    }
  }



  /**
   * append the Group By part of the query to <code>stringBuilder</code>.
   * 
   * @throws TechnicalIOException
   *           never
   */
  private void appendGroupBy()
      throws TechnicalIOException
  {
    if (getGroupBys() != null && !getGroupBys().isEmpty()) {
      getCollectionManager().join(stringBuilder, getGroupBys(), GROUP_BY_DELIMITER, getParametrizableQueryableDisplayObject());
    }
  }



  /**
   * append the Order by part of the query to <code>stringBuilder</code>.
   * 
   * @throws TechnicalIOException
   *           never
   */
  private void appendOrderBy()
      throws TechnicalIOException
  {
    if (getOrders() != null && !getOrders().isEmpty()) {
      getCollectionManager().join(stringBuilder, getOrders(), ORDER_BY_DELIMITER, getParametrizableQueryableDisplayObject());
    }
  }



  /**
   * append the Select part of the query to <code>stringBuilder</code>.
   * 
   * @throws TechnicalIOException
   *           never
   */
  private void appendSelect()
      throws TechnicalIOException
  {
    if (getProjections() != null && !getProjections().isEmpty()) {
      getCollectionManager().join(stringBuilder, getProjections().values(), isDistinct() ? SELECT_DISTINCT_DELIMITER : SELECT_DELIMITER, getUnParametrizableQueryableDisplayObject());
    }
    else if (isDistinct()) {
      getCollectionManager().join(stringBuilder, getBaseQuery(), SELECT_DISTINCT_DELIMITER, SIMPLE_SELECT_PRINTER);
    }

  }



  /**
   * append the Where part of the query to <code>stringBuilder</code>.
   */
  private void appendWhere()
  {
    if (getRestrictions() != null) {
      stringBuilder.append(" WHERE ");
      ((HibernateCriterion) getRestrictions()).appendQuery(true, getCollectionManager(), this, stringBuilder, getParametrizableQueryableDisplayObject());
    }
  }



  /**
   * Fill the parameters <code>Map</code> with the constants elements of the
   * <code>Queryable</code> passed in arguments.
   * 
   * @param parameters
   *          the <code>Map</code> to be filled
   * @param fields
   *          the <code>Queryable</code> in which to look for parameters
   */
  private void fillParameters(final Map<String, Object> parameters, final Collection<? extends Queryable> fields)
  {
    if (fields == null || fields.isEmpty()) {
      return;
    }
    for (final Queryable field : fields) {
      field.fillParameters(parameters);
    }
  }



  /**
   * getter for the parametrizableQueryableDisplayObject property.
   * 
   * @return the parametrizableQueryableDisplayObject
   */
  private QueryableDisplayObject getParametrizableQueryableDisplayObject()
  {
    if (parametrizableQueryableDisplayObject == null) {
      parametrizableQueryableDisplayObject = new QueryableDisplayObject(true, getCollectionManager(), this);
    }
    return parametrizableQueryableDisplayObject;
  }



  /**
   * getter for the unParametrizableQueryableDisplayObject property.
   * 
   * @return the unParametrizableQueryableDisplayObject
   */
  private QueryableDisplayObject getUnParametrizableQueryableDisplayObject()
  {
    if (unParametrizableQueryableDisplayObject == null) {
      unParametrizableQueryableDisplayObject = new QueryableDisplayObject(false, getCollectionManager(), this);
    }
    return unParametrizableQueryableDisplayObject;
  }

}
